/*****************************************************************************
*
*  PROJECT:     Multi Theft Auto v1.2
*  LICENSE:     See LICENSE in the top level directory
*  FILE:        FileSystem/src/CFileSystem.zip.translator.cpp
*  PURPOSE:     ZIP archive filesystem
*
*  Docu: https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html
*
*  Multi Theft Auto is available from http://www.multitheftauto.com/
*
*****************************************************************************/

#include <StdInc.h>
#include <zlib.h>
#include <sys/stat.h>

// Include internal (private) definitions.
#include "fsinternal/CFileSystem.internal.h"
#include "fsinternal/CFileSystem.zip.internal.h"

extern CFileSystem *fileSystem;

#include "CFileSystem.Utils.hxx"
#include "CFileSystem.zip.utils.hxx"

using namespace FileSystem;

/*=======================================
    CZIPArchiveTranslator::fileDeflate

    ZIP file seeking and handling
=======================================*/

CZIPArchiveTranslator::fileDeflate::fileDeflate( CZIPArchiveTranslator& zip, file& info, CFile& sysFile, bool destroyOnClose ) : stream( zip, info, sysFile, destroyOnClose )
{
}

CZIPArchiveTranslator::fileDeflate::~fileDeflate( void )
{
}

size_t CZIPArchiveTranslator::fileDeflate::Read( void *buffer, size_t sElement, size_t iNumElements )
{
    if ( !m_readable )
        return 0;

    // TODO: add locks to files so that they can be read (seek + op) without problems
    size_t realReadCount;
    {
        this->PutSeek();

        realReadCount = m_sysFile.Read( buffer, sElement, iNumElements );

        this->UpdateSeek();
    }
    return realReadCount;
}

size_t CZIPArchiveTranslator::fileDeflate::Write( const void *buffer, size_t sElement, size_t iNumElements )
{
    if ( !m_writeable )
        return 0;

    m_info.metaData.UpdateTime();

    // We now have local mods.
    m_info.metaData.hasDataLocalMods = true;
    
    // TODO: add locks to prevent problems of multi-threading when setting the seek to the shared file handle.
    size_t realWriteCount;
    {
        this->PutSeek();

        realWriteCount = m_sysFile.Write( buffer, sElement, iNumElements );

        this->UpdateSeek();
    }
    return realWriteCount;
}

int CZIPArchiveTranslator::fileDeflate::Seek( long iOffset, int iType )
{
    long base = 0;

    if ( iType == SEEK_SET )
    {
        base = 0;
    }
    else if ( iType == SEEK_CUR )
    {
        base = (long)this->m_ourSeek;
    }
    else if ( iType == SEEK_END )
    {
        base = (long)this->m_sysFile.GetSize();
    }
    else
    {
        return -1;
    }

    fsOffsetNumber_t newSeek = (fsOffsetNumber_t)( base + iOffset );

    this->m_ourSeek = newSeek;

    return 0;
}

int CZIPArchiveTranslator::fileDeflate::SeekNative( fsOffsetNumber_t iOffset, int iType )
{
    fsOffsetNumber_t base = 0;

    if ( iType == SEEK_SET )
    {
        base = 0;
    }
    else if ( iType == SEEK_CUR )
    {
        base = this->m_ourSeek;
    }
    else if ( iType == SEEK_END )
    {
        base = this->m_sysFile.GetSizeNative();
    }
    else
    {
        return -1;
    }

    fsOffsetNumber_t newSeek = ( base + iOffset );

    this->m_ourSeek = newSeek;

    return 0;
}

long CZIPArchiveTranslator::fileDeflate::Tell( void ) const noexcept
{
    return (long)this->m_ourSeek;
}

fsOffsetNumber_t CZIPArchiveTranslator::fileDeflate::TellNative( void ) const noexcept
{
    return this->m_ourSeek;
}

bool CZIPArchiveTranslator::fileDeflate::IsEOF( void ) const noexcept
{
    return ( this->m_ourSeek >= this->GetSizeNative() );
}

bool CZIPArchiveTranslator::fileDeflate::Stat( struct stat *stats ) const
{
    // TODO: debug this, because there are some bugs regarding correct date display.

    // Fill out the boring things.
    stats->st_dev = 0;
    stats->st_ino = 0;
    
    unsigned short mode = S_IFREG;

    if ( this->m_readable )
    {
        mode |= S_IREAD;
    }

    if ( this->m_writeable )
    {
        mode |= S_IWRITE;
    }

    stats->st_mode = mode;
    stats->st_nlink = 1;
    stats->st_uid = 0;
    stats->st_gid = 0;
    stats->st_rdev = 0;
    stats->st_size = (off_t)( this->m_sysFile.GetSize() );

    tm date;

    m_info.metaData.GetModTime( date );

    date.tm_year -= 1900;

    time_t timeval = mktime( &date );

    stats->st_mtime = timeval;
    stats->st_atime = timeval;
    stats->st_ctime = timeval;
    return true;
}

#pragma warning(push)
#pragma warning(disable: 4996)

void CZIPArchiveTranslator::fileDeflate::PushStat( const struct stat *stats )
{
    tm *date = gmtime( &stats->st_mtime );

    m_info.metaData.SetModTime( *date );
}

#pragma warning(pop)

void CZIPArchiveTranslator::fileDeflate::SetSeekEnd( void )
{
    // TODO: add locking here, too.

    this->m_info.metaData.UpdateTime();

    // We now have local mods.
    this->m_info.metaData.hasDataLocalMods = true;

    this->PutSeek();

    m_sysFile.SetSeekEnd();
}

size_t CZIPArchiveTranslator::fileDeflate::GetSize( void ) const noexcept
{
    return m_sysFile.GetSize();
}

fsOffsetNumber_t CZIPArchiveTranslator::fileDeflate::GetSizeNative( void ) const noexcept
{
    return m_sysFile.GetSizeNative();
}

void CZIPArchiveTranslator::fileDeflate::Flush( void )
{
    m_sysFile.Flush();
}

bool CZIPArchiveTranslator::fileDeflate::IsReadable( void ) const noexcept
{
    return m_readable;
}

bool CZIPArchiveTranslator::fileDeflate::IsWriteable( void ) const noexcept
{
    return m_writeable;
}

/*=======================================
    CZIPArchiveTranslator

    ZIP translation utility
=======================================*/

CZIPArchiveTranslator::CZIPArchiveTranslator( CFileSystemNative *fileSys, zipExtension& theExtension, CFile& fileStream ) : CSystemPathTranslator( false ), m_zipExtension( theExtension ), m_file( fileStream ), m_virtualFS( true ), fileMan( fileSys )
{
    // TODO: Get real .zip structure offset
    m_structOffset = 0;

    // Set up the file presence management.
    fileMan.SetMaximumDataQuotaRAM( 100000000 );    // about 100mb.

    // Set up the virtual filesystem.
    m_virtualFS.hostTranslator = this;
}

CZIPArchiveTranslator::~CZIPArchiveTranslator( void )
{
    return;
}

bool CZIPArchiveTranslator::CreateDir( const char *path )
{
    return m_virtualFS.CreateDir( path );
}

CFile* CZIPArchiveTranslator::OpenNativeFileStream( file *fsObject, eFileMode openMode, unsigned int access )
{
    CFile *dstFile = NULL;
    bool isSpecialLinkFile = false;

    if ( openMode == eFileMode::OPEN )
    {
        const filePath& relPath = fsObject->relPath;

        eFileDataState dataState = fsObject->metaData.dataState;

        // Attempt to get a handle to the realtime root.
        if ( dataState == eFileDataState::ARCHIVED ||
             dataState == eFileDataState::PRESENT_COMPRESSED )
        {
            dstFile = this->fileMan.AllocateTemporaryDataDestination();

            if ( dstFile )
            {
                try
                {
                    Extract( *dstFile, *fsObject );
                }
                catch( ... )
                {
                    delete dstFile;

                    throw;
                }

                // If we have a previous data stream, remove it.
                // This mostly happens if we decompress a compressed-in-memory file.
                fsObject->metaData.SetDataStream( dstFile );
                fsObject->metaData.dataState = eFileDataState::PRESENT;
                fsObject->metaData.hasDataLocalMods = false;

                // We are also gonna use this stream, of course.
                fsObject->metaData.AcquireDataStream();

                isSpecialLinkFile = false;
            }
        }
        else if ( dataState == eFileDataState::PRESENT )
        {
            dstFile = fsObject->metaData.AcquireDataStream();

            isSpecialLinkFile = false;
        }
        else
        {
            return NULL;
        }
    }
    else if ( openMode == eFileMode::CREATE )
    {
        dstFile = this->fileMan.AllocateTemporaryDataDestination();

        if ( dstFile )
        {
            fsObject->metaData.SetDataStream( dstFile );
            fsObject->metaData.dataState = eFileDataState::PRESENT;
            fsObject->metaData.hasDataLocalMods = true;

            // Actually needing the file aswell.
            fsObject->metaData.AcquireDataStream();

            isSpecialLinkFile = false;
        }
    }
    else
    {
        // TODO: implement any unknown cases.
        return NULL;
    }

    CFile *outputFile = NULL;

    if ( dstFile )
    {
        fileDeflate *f = NULL;

        try
        {
            f = new fileDeflate( *this, *fsObject, *dstFile, ( isSpecialLinkFile == true ) );
        }
        catch( ... )
        {
            if ( isSpecialLinkFile )
            {
                delete dstFile;
            }
            else
            {
                fsObject->metaData.ReleaseDataStream();
            }

            throw;
        }

        if ( f )
        {
            try
            {
                f->m_path = fsObject->relPath;
                f->m_writeable = ( access & FILE_ACCESS_WRITE ) != 0;
                f->m_readable = ( access & FILE_ACCESS_READ ) != 0;

                // Update the stat
                {
                    struct stat info;
                    bool statGetSuccess = f->Stat( &info );
                
                    if ( statGetSuccess )
                    {
                        dstFile->PushStat( &info );
                    }
                }
            }
            catch( ... )
            {
                delete f;

                throw;
            }

            outputFile = f;
        }
    }
    return outputFile;
}

CFile* CZIPArchiveTranslator::Open( const char *path, const char *mode, eFileOpenFlags flags )
{
    return m_virtualFS.OpenStream( path, mode );
}

bool CZIPArchiveTranslator::Exists( const char *path ) const
{
    return m_virtualFS.Exists( path );
}

void CZIPArchiveTranslator::fileMetaData::OnFileDelete( void )
{
    // Delete the resources that are associated with this file entry.
    if ( CFile *dataStream = this->dataStream )
    {
        delete dataStream;

        this->dataStream = NULL;
    }

    this->dataState = eFileDataState::ARCHIVED;
}

bool CZIPArchiveTranslator::Delete( const char *path )
{
    return m_virtualFS.Delete( path );
}

bool CZIPArchiveTranslator::fileMetaData::OnFileCopy( const dirTree& tree, const filePath& newName ) const
{
    // We have no more meta-data that need preparing on external storage devices.
    return true;
}

void CZIPArchiveTranslator::OnRemoveRemoteFile( const dirTree& tree, const filePath& newName )
{
    // Since we do not use remote file storage anymore, this is a no-op.
    return;
}

bool CZIPArchiveTranslator::fileMetaData::CopyAttributesTo( fileMetaData& dstEntry )
{
    CZIPArchiveTranslator *trans = this->translator;

    // Copy over general attributes
    dstEntry.flags          = flags;
    dstEntry.compression    = compression;
    dstEntry.modTime        = modTime;
    dstEntry.modDate        = modDate;
    dstEntry.diskID         = diskID;
    dstEntry.internalAttr   = internalAttr;
    dstEntry.externalAttr   = externalAttr;
    dstEntry.extra          = extra;
    dstEntry.comment        = comment;
    dstEntry.dataState      = dataState;

    if ( dataState == eFileDataState::ARCHIVED )
    {
        dstEntry.version            = version;
        dstEntry.reqVersion         = reqVersion;
        dstEntry.crc32val           = crc32val;
        dstEntry.sizeCompressed     = sizeCompressed;
        dstEntry.sizeReal           = sizeReal;
    }

    // Also copy the file data.
    {
        CFile *dstDataStream = NULL;

        if ( CFile *srcDataStream = this->dataStream )
        {
            dstDataStream = trans->fileMan.AllocateTemporaryDataDestination();

            if ( !dstDataStream )
            {
                return false;
            }

            try
            {
                srcDataStream->Seek( 0, SEEK_SET );
            
                FileSystem::StreamCopy( *srcDataStream, *dstDataStream );
            }
            catch( ... )
            {
                delete dstDataStream;

                throw;
            }
        }

        dstEntry.dataStream = dstDataStream;
    }

    return true;
}

bool CZIPArchiveTranslator::Copy( const char *src, const char *dst )
{
    return m_virtualFS.Copy( src, dst );
}

bool CZIPArchiveTranslator::fileMetaData::OnFileRename( const dirTree& tree, const filePath& newName )
{
    // We have no more meta-data that needs movement based on the object's filename.
    return true;
}

bool CZIPArchiveTranslator::Rename( const char *src, const char *dst )
{
    return m_virtualFS.Rename( src, dst );
}

size_t CZIPArchiveTranslator::Size( const char *path ) const
{
    return (size_t)m_virtualFS.Size( path );
}

bool CZIPArchiveTranslator::Stat( const char *path, struct stat *stats ) const
{
    return m_virtualFS.Stat( path, stats );
}

bool CZIPArchiveTranslator::OnConfirmDirectoryChange( const dirTree& tree )
{
    return m_virtualFS.ChangeDirectory( tree );
}

static void _scanFindCallback( const filePath& path, std::vector <filePath> *output )
{
    output->push_back( path );
}

void CZIPArchiveTranslator::ScanDirectory( const char *path, const char *wildcard, bool recurse, pathCallback_t dirCallback, pathCallback_t fileCallback, void *userdata ) const
{
    m_virtualFS.ScanDirectory( path, wildcard, recurse, dirCallback, fileCallback, userdata );
}

void CZIPArchiveTranslator::GetDirectories( const char *path, const char *wildcard, bool recurse, std::vector <filePath>& output ) const
{
    ScanDirectory( path, wildcard, recurse, (pathCallback_t)_scanFindCallback, NULL, &output );
}

void CZIPArchiveTranslator::GetFiles( const char *path, const char *wildcard, bool recurse, std::vector <filePath>& output ) const
{
    ScanDirectory( path, wildcard, recurse, NULL, (pathCallback_t)_scanFindCallback, &output );
}

void CZIPArchiveTranslator::ReadFiles( unsigned int count )
{
    char buf[65536];

    while ( count-- )
    {
        _centralFile header;

        if ( !m_file.ReadStruct( header ) || header.signature != ZIP_FILE_SIGNATURE )
        {
            throw filesystem_exception( eGenExceptCode::INTERNAL_ERROR );
        }

        // Acquire the path
        m_file.Read( buf, 1, header.nameLen );
        buf[ header.nameLen ] = 0;

        dirTree tree;
        bool isFile;

        if ( !GetRelativePathTreeFromRoot( buf, tree, isFile ) )
        {
            throw filesystem_exception( eGenExceptCode::INTERNAL_ERROR );
        }

        union
        {
            genericMetaData *fsObject;
            fileMetaData *fileEntry;
            directoryMetaData *dirEntry;
        };
        fsObject = NULL;

        if ( isFile )
        {
            filePath name = std::move( tree.back() );
            tree.pop_back();

            // Deposit in the correct directory
            fileEntry = &m_virtualFS._CreateDirTree( m_virtualFS.GetRootDir(), tree ).AddFile( std::move( name ) ).metaData;
        }
        else
        {
            // Try to create the directory.
            dirEntry = &m_virtualFS._CreateDirTree( m_virtualFS.GetRootDir(), tree ).metaData;
        }

        // Initialize common data.
        if ( fsObject )
        {
            fsObject->version = header.version;
            fsObject->reqVersion = header.reqVersion;
            fsObject->flags = header.flags;
            fsObject->modTime = header.modTime;
            fsObject->modDate = header.modDate;
            fsObject->diskID = header.diskID;
            fsObject->localHeaderOffset = header.localHeaderOffset;

            if ( header.extraLen != 0 )
            {
                m_file.Read( buf, 1, header.extraLen );
                fsObject->extra = std::string( buf, header.extraLen );
            }

            if ( header.commentLen != 0 )
            {
                m_file.Read( buf, 1, header.commentLen );
                fsObject->comment = std::string( buf, header.commentLen );
            }
        }
        else
        {
            m_file.SeekNative( header.extraLen + header.commentLen, SEEK_CUR );
        }

        if ( fileEntry && isFile )
        {
            // Store file-orriented attributes
            fileEntry->compression = header.compression;
            fileEntry->crc32val = header.crc32val;
            fileEntry->sizeCompressed = header.sizeCompressed;
            fileEntry->sizeReal = header.sizeReal;
            fileEntry->internalAttr = header.internalAttr;
            fileEntry->externalAttr = header.externalAttr;
            fileEntry->dataState = eFileDataState::ARCHIVED;
        }
    }
}

inline void CZIPArchiveTranslator::seekFile( const fileMetaData& info, _localHeader& header )
{
    m_file.SeekNative( info.localHeaderOffset, SEEK_SET );

    if ( !m_file.ReadStruct( header ) )
    {
        // TODO: add special ZIP exceptions.
        throw filesystem_exception( eGenExceptCode::INTERNAL_ERROR );
    }

    if ( header.signature != ZIP_LOCAL_FILE_SIGNATURE )
    {
        throw filesystem_exception( eGenExceptCode::INTERNAL_ERROR );
    }

    m_file.SeekNative( header.nameLen + header.commentLen, SEEK_CUR );
}

struct zip_inflate_decompression
{
    zip_inflate_decompression( bool hasHeader )
    {
        m_stream.zalloc = NULL;
        m_stream.zfree = NULL;
        m_stream.opaque = NULL;

        if ( inflateInit2( &m_stream, hasHeader ? MAX_WBITS : -MAX_WBITS ) != Z_OK )
        {
            throw filesystem_exception( eGenExceptCode::INTERNAL_ERROR );
        }
    }

    ~zip_inflate_decompression( void )
    {
        inflateEnd( &m_stream );
    }

    inline void prepare( const char *buf, size_t size, bool eof )
    {
        m_stream.avail_in = (uInt)size;
        m_stream.next_in = (Bytef*)buf;
    }

    inline bool parse( char *buf, size_t size, size_t& sout )
    {
        m_stream.avail_out = (uInt)size;
        m_stream.next_out = (Bytef*)buf;

        switch( inflate( &m_stream, Z_NO_FLUSH ) )
        {
        case Z_DATA_ERROR:
        case Z_NEED_DICT:
            throw filesystem_exception( eGenExceptCode::INTERNAL_ERROR );
        case Z_MEM_ERROR:
            throw filesystem_exception( eGenExceptCode::MEMORY_INSUFFICIENT );            
        }

        sout = size - m_stream.avail_out;
        return m_stream.avail_out == 0;
    }

    z_stream m_stream;
};

void CFileSystem::DecompressZLIBStream( CFile *input, CFile *output, size_t inputSize, bool hasHeader ) const
{
    zip_inflate_decompression decompressor( hasHeader );

    FileSystem::StreamParserCount( *input, *output, inputSize, decompressor );
}

void CZIPArchiveTranslator::Extract( CFile& dstFile, file& info )
{
    CFile *from = NULL;
    size_t comprSize = info.metaData.sizeCompressed;

    eFileDataState dataState = info.metaData.dataState;

    bool requiresDataStreamRelease = false;

    if ( dataState == eFileDataState::PRESENT_COMPRESSED )
    {
        CFile *srcDataStream = info.metaData.AcquireDataStream();

        assert( srcDataStream != &dstFile );

        try
        {
            srcDataStream->Seek( 0, SEEK_SET );
        }
        catch( ... )
        {
            info.metaData.ReleaseDataStream();

            throw;
        }

        requiresDataStreamRelease = true;

        from = srcDataStream;
    }
    else if ( dataState == eFileDataState::ARCHIVED )
    {
        _localHeader header;
        seekFile( info.metaData, header );

        from = &m_file;
    }
    else
    {
        // We are already (fully) extracted.
        return;
    }

    assert( from != NULL );

    try
    {
        if ( info.metaData.compression == 0 )
        {
            FileSystem::StreamCopyCount( *from, dstFile, comprSize );
        }
        else if ( info.metaData.compression == 8 )
        {
            zip_inflate_decompression decompressor( false );

            FileSystem::StreamParserCount( *from, dstFile, comprSize, decompressor );
        }
        else
        {
            // TODO: add support for more compression types.
            throw filesystem_exception( eGenExceptCode::INVALID_PARAM );
        }

        dstFile.SetSeekEnd();
    }
    catch( ... )
    {
        if ( requiresDataStreamRelease )
        {
            info.metaData.ReleaseDataStream();
        }

        throw;
    }

    if ( requiresDataStreamRelease )
    {
        info.metaData.ReleaseDataStream();
    }
}

void CZIPArchiveTranslator::CacheDirectory( const directory& dir )
{
    for ( file *fileEntry : dir.files )
    {
        // Dump the compressed content
        // This is important because we will rebuild the archive.
        if ( fileEntry->metaData.dataState == eFileDataState::ARCHIVED )
        {
            _localHeader header;
            seekFile( fileEntry->metaData, header );

            CFile *dst = this->fileMan.AllocateTemporaryDataDestination();

            if ( !dst )
            {
                throw filesystem_exception( eGenExceptCode::RESOURCE_UNAVAILABLE );
            }

            try
            {
                FileSystem::StreamCopyCount( m_file, *dst, fileEntry->metaData.sizeCompressed );

                dst->SetSeekEnd();
            }
            catch( ... )
            {
                delete dst;

                throw;
            }

            fileEntry->metaData.SetDataStream( dst );
            fileEntry->metaData.dataState = eFileDataState::PRESENT_COMPRESSED;
            fileEntry->metaData.hasDataLocalMods = false;
        }
    }

    for ( directory *dir : dir.children )
    {
        CacheDirectory( *dir );
    }
}

struct compression_progress
{
    fsUInt_t sizeCompressed;
    fsUInt_t crc32val;
};

struct zip_stream_compression
{
    zip_stream_compression( compression_progress& header ) : m_header( header )
    {
        m_header.sizeCompressed = 0;
        m_header.crc32val = 0;
    }

    inline void checksum( const char *buf, size_t size )
    {
        m_header.crc32val = crc32( m_header.crc32val, (Bytef*)buf, (uInt)size );
    }

    inline void prepare( const char *buf, size_t size, bool eof )
    {
        m_rcv = size;
        m_buf = buf;

        checksum( buf, size );
    }

    inline bool parse( char *buf, size_t size, size_t& sout )
    {
        size_t toWrite = std::min( size, m_rcv );
        memcpy( buf, m_buf, toWrite );

        m_header.sizeCompressed += (uInt)toWrite;

        m_buf += toWrite;
        m_rcv -= toWrite;

        sout = size;
        return m_rcv != 0;
    }

    size_t m_rcv;
    const char* m_buf;
    compression_progress& m_header;
};

struct zip_deflate_compression : public zip_stream_compression
{
    zip_deflate_compression( compression_progress& header, int level, bool putHeader ) : zip_stream_compression( header )
    {
        m_stream.zalloc = NULL;
        m_stream.zfree = NULL;
        m_stream.opaque = NULL;

        if ( deflateInit2( &m_stream, level, Z_DEFLATED, putHeader ? MAX_WBITS : -MAX_WBITS, 8, Z_DEFAULT_STRATEGY ) != Z_OK )
        {
            throw filesystem_exception( eGenExceptCode::INTERNAL_ERROR );
        }
    }

    ~zip_deflate_compression( void )
    {
        deflateEnd( &m_stream );
    }

    inline void prepare( const char *buf, size_t size, bool eof )
    {
        m_flush = eof ? Z_FINISH : Z_NO_FLUSH;

        m_stream.avail_in = (uInt)size;
        m_stream.next_in = (Bytef*)buf;

        zip_stream_compression::checksum( buf, size );
    }

    inline bool parse( char *buf, size_t size, size_t& sout )
    {
        m_stream.avail_out = (uInt)size;
        m_stream.next_out = (Bytef*)buf;

        int ret = deflate( &m_stream, m_flush );

        sout = size - m_stream.avail_out;

        m_header.sizeCompressed += (uInt)sout;

        if ( ret == Z_STREAM_ERROR )
        {
            throw filesystem_exception( eGenExceptCode::INTERNAL_ERROR );
        }

        return m_stream.avail_out == 0;
    }

    int m_flush;
    z_stream m_stream;
};

void CFileSystem::CompressZLIBStream( CFile *input, CFile *output, bool putHeader ) const
{
    compression_progress progress;
    zip_deflate_compression compressor( progress, Z_DEFAULT_COMPRESSION, putHeader );

    FileSystem::StreamParser( *input, *output, compressor );
}

static inline void WriteStreamString( CFile *stream, const std::string& string )
{
    stream->Write( string.c_str(), 1, string.size() );
}

void CZIPArchiveTranslator::SaveDirectory( directory& dir, size_t& size )
{
    if ( dir.metaData.NeedsWriting() )
    {
        _localHeader header = dir.metaData.ConstructLocalHeader();
        
        // Allocate space in the archive.
        dir.metaData.AllocateArchiveSpace( this, header, size );

        header.version = dir.metaData.version;
        header.flags = dir.metaData.flags;
        header.compression = 0;
        header.crc32val = 0;
        header.sizeCompressed = 0;
        header.sizeReal = 0;

        m_file.WriteStruct( header );
        WriteStreamString( &m_file, dir.relPath );
        WriteStreamString( &m_file, dir.metaData.comment );
    }
    
    directory::subDirs::iterator iter = dir.children.begin();

    for ( ; iter != dir.children.end(); ++iter )
    {
        SaveDirectory( **iter, size );
    }

    for ( file *pinfo : dir.files )
    {
        file& info = *pinfo;

        _localHeader header = info.metaData.ConstructLocalHeader();

        // Allocate space in the archive.
        info.metaData.AllocateArchiveSpace( this, header, size );

        eFileDataState dataState = info.metaData.dataState;

        if ( dataState == eFileDataState::PRESENT_COMPRESSED )
        {
            header.version          = info.metaData.version;
            header.flags            = info.metaData.flags;
            header.compression      = info.metaData.compression;
            header.crc32val         = info.metaData.crc32val;
            header.sizeCompressed   = (fsUInt_t)info.metaData.sizeCompressed;
            header.sizeReal         = (fsUInt_t)info.metaData.sizeReal;

            size += info.metaData.sizeCompressed;

            m_file.WriteStruct( header );
            WriteStreamString( &m_file, info.relPath );
            WriteStreamString( &m_file, info.metaData.comment );

            CFile *src = info.metaData.AcquireDataStream();

            if ( src )
            {
                try
                {
                    src->Seek( 0, SEEK_SET );

                    FileSystem::StreamCopy( *src, m_file );

                    m_file.SetSeekEnd();
                }
                catch( ... )
                {
                    info.metaData.ReleaseDataStream();

                    throw;
                }

                info.metaData.ReleaseDataStream();
            }
        }
        else if ( dataState == eFileDataState::PRESENT )
        {
            header.version      = 10;    // WINNT
            header.flags        = info.metaData.flags;
            header.compression  = info.metaData.compression = 8; // deflate
            header.crc32val     = 0;

            m_file.WriteStruct( header );
            WriteStreamString( &m_file, info.relPath );
            WriteStreamString( &m_file, info.metaData.comment );

            CFile *src = info.metaData.AcquireDataStream();

            size_t actualFileSize = 0;
            fsUInt_t sizeCompressed = 0;
            fsUInt_t crc32val = 0;

            if ( src )
            {
                try
                {
                    src->Seek( 0, SEEK_SET );

                    actualFileSize = src->GetSize();

                    compression_progress c_prog;

                    if ( header.compression == 0 )
                    {
                        zip_stream_compression compressor( c_prog );

                        FileSystem::StreamParser( *src, m_file, compressor );
                    }
                    else if ( header.compression == 8 )
                    {
                        zip_deflate_compression compressor( c_prog, Z_DEFAULT_COMPRESSION, false );

                        FileSystem::StreamParser( *src, m_file, compressor );
                    }
                    else
                    {
                        throw filesystem_exception( eGenExceptCode::INVALID_PARAM );
                    }

                    // Finalize the compression information.
                    sizeCompressed = c_prog.sizeCompressed;
                    crc32val = c_prog.crc32val;
                }
                catch( ... )
                {
                    info.metaData.ReleaseDataStream();

                    throw;
                }

                info.metaData.ReleaseDataStream();
            }

            info.metaData.sizeReal = actualFileSize;
            info.metaData.sizeCompressed = sizeCompressed;
            info.metaData.crc32val = crc32val;

            header.sizeReal = (fsUInt_t)actualFileSize;
            header.sizeCompressed = sizeCompressed;
            header.crc32val = crc32val;

            size += sizeCompressed;

            fsOffsetNumber_t wayOff = sizeCompressed + header.nameLen + header.commentLen;

            m_file.SeekNative( -wayOff - (fsOffsetNumber_t)sizeof( header ), SEEK_CUR );
            m_file.WriteStruct( header );
            m_file.SeekNative( wayOff, SEEK_CUR );
        }
        else
        {
            // Must not happen.
            throw filesystem_exception( eGenExceptCode::INTERNAL_ERROR );
        }
    }
}

unsigned int CZIPArchiveTranslator::BuildCentralFileHeaders( const directory& dir, size_t& size )
{
    unsigned int cnt = 0;

    if ( dir.metaData.NeedsWriting() )
    {
        _centralFile header = dir.metaData.ConstructCentralHeader();

        // Zero out fields which make no sense
        header.compression = 0;
        header.crc32val = 0;
        header.sizeCompressed = 0;
        header.sizeReal = 0;
        header.internalAttr = 0;
        header.externalAttr = 0x10;     // FILE_ATTRIBUTE_DIRECTORY (win32)
        
        m_file.WriteStruct( header );
        WriteStreamString( &m_file, dir.relPath );
        WriteStreamString( &m_file, dir.metaData.extra );
        WriteStreamString( &m_file, dir.metaData.comment );
        
        size += sizeof( header ) + header.nameLen + header.extraLen + header.commentLen;
        cnt++;
    }

    for ( const directory *dir : dir.children )
    {
        cnt += BuildCentralFileHeaders( *dir, size );
    }

    for ( const file *info : dir.files )
    {
        _centralFile header = info->metaData.ConstructCentralHeader();

        header.compression      = info->metaData.compression;
        header.crc32val         = info->metaData.crc32val;
        header.sizeCompressed   = (fsUInt_t)info->metaData.sizeCompressed;
        header.sizeReal         = (fsUInt_t)info->metaData.sizeReal;
        header.internalAttr     = info->metaData.internalAttr;
        header.externalAttr     = info->metaData.externalAttr;

        m_file.WriteStruct( header );
        WriteStreamString( &m_file, info->relPath );
        WriteStreamString( &m_file, info->metaData.extra );
        WriteStreamString( &m_file, info->metaData.comment );

        size += sizeof( header ) + header.nameLen + header.extraLen + header.commentLen;
        cnt++;
    }

    return cnt;
}

void CZIPArchiveTranslator::Save( void )
{
    if ( !m_file.IsWriteable() )
        return;

    // Cache the .zip content
    CacheDirectory( m_virtualFS.GetRootDir() );

    // Rewrite the archive
    m_file.SeekNative( m_structOffset, SEEK_SET );

    size_t fileSize = 0;
    SaveDirectory( m_virtualFS.GetRootDir(), fileSize );

    // Create the central directory
    size_t centralSize = 0;
    unsigned int entryCount = BuildCentralFileHeaders( m_virtualFS.GetRootDir(), centralSize );

    // Finishing entry
    m_file.WriteInt( ZIP_SIGNATURE );

    _endDir tail;
    tail.diskID = 0;
    tail.diskAlign = 0;
    tail.entries = entryCount;
    tail.totalEntries = entryCount;
    tail.centralDirectorySize = (fsUInt_t)centralSize;
    tail.centralDirectoryOffset = (fsUInt_t)fileSize; // we might need the offset of the .zip here
    tail.commentLen = (fsUShort_t)m_comment.size();

    m_file.WriteStruct( tail );
    WriteStreamString( &m_file, m_comment );

    // Cap the stream
    m_file.SetSeekEnd();
}